import React from 'react';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { fireEvent, waitFor } from '@testing-library/react';
import { EthAccountType, EthScope } from '@metamask/keyring-api';
import nock from 'nock';
import {
  CHAIN_IDS,
  MAINNET_DISPLAY_NAME,
} from '../../../../shared/constants/network';
import { renderWithProvider } from '../../../../test/lib/render-helpers-navigate';
import { KeyringType } from '../../../../shared/constants/keyring';
import { AssetType } from '../../../../shared/constants/transaction';
import { ETH_EOA_METHODS } from '../../../../shared/constants/eth-methods';
import { setBackgroundConnection } from '../../../store/background-connection';
import {
  mockNetworkState,
  mockMultichainNetworkState,
} from '../../../../test/stub/networks';
import useMultiPolling from '../../../hooks/useMultiPolling';
import AssetPage from './asset-page';

jest.mock('../../../store/actions', () => ({
  ...jest.requireActual('../../../store/actions'),
  tokenBalancesStartPolling: jest.fn().mockResolvedValue('pollingToken'),
  tokenBalancesStopPollingByPollingToken: jest.fn(),
}));

jest.mock('../../../store/controller-actions/transaction-controller');

// Mock the price chart
jest.mock('react-chartjs-2', () => ({ Line: () => null }));

// Mock BUYABLE_CHAINS_MAP
jest.mock('../../../../shared/constants/network', () => ({
  ...jest.requireActual('../../../../shared/constants/network'),
  BUYABLE_CHAINS_MAP: {
    // MAINNET
    '0x1': {
      nativeCurrency: 'ETH',
      network: 'ethereum',
    },
    // POLYGON
    '0x89': {
      nativeCurrency: 'MATIC',
      network: 'polygon',
    },
  },
}));

jest.mock('../../../hooks/useMultiPolling', () => ({
  // TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860
  // eslint-disable-next-line @typescript-eslint/naming-convention
  __esModule: true,
  default: jest.fn(),
}));

const selectedAccountAddress = 'cf8dace4-9439-4bd4-b3a8-88c821c8fcb3';

jest.mock('../../../selectors/assets', () => ({
  ...jest.requireActual('../../../selectors/assets'),
  getAssetsBySelectedAccountGroup: jest.fn().mockReturnValue({
    '0x1': [
      {
        assetId: '0x0000000000000000000000000000000000000000',
        rawBalance: '0x0',
        balance: '0',
        fiat: {
          balance: 0,
        },
      },
      {
        assetId: '0x309375769E79382beFDEc5bdab51063AeBDC4936',
        rawBalance: '0x0',
        balance: '0',
        fiat: {
          balance: 0,
        },
      },
      {
        assetId: '0xe4246B1Ac0Ba6839d9efA41a8A30AE3007185f55',
        rawBalance: '0x0',
        balance: '0',
        fiat: {
          balance: 0,
        },
      },
    ],
  }),
}));

describe('AssetPage', () => {
  const mockStore = {
    localeMessages: {
      currentLocale: 'en',
    },
    appState: {
      confirmationExchangeRates: {},
    },
    metamask: {
      ...mockMultichainNetworkState(),
      txHistory: {},
      remoteFeatureFlags: {
        bridgeConfig: {
          support: true,
        },
      },
      tokenList: {},
      tokenBalances: {
        [selectedAccountAddress]: {
          [CHAIN_IDS.MAINNET]: {},
        },
      },
      marketData: {},
      allTokens: {},
      accountsByChainId: {
        '0x1': {
          [selectedAccountAddress]: {
            address: selectedAccountAddress,
            balance: '0x00',
          },
        },
      },
      currentCurrency: 'usd',
      accounts: {},
      ...mockNetworkState({ chainId: CHAIN_IDS.MAINNET }),
      currencyRates: {
        TEST: {
          conversionRate: 123,
          ticker: 'ETH',
        },
        ETH: {
          conversionRate: 123,
          ticker: 'ETH',
        },
      },
      useCurrencyRateCheck: true,
      preferences: {},
      enabledNetworkMap: {
        eip155: {},
      },
      accountTree: {
        wallets: {
          'entropy:01JKAF3DSGM3AB87EM9N0K41AJ': {
            id: 'entropy:01JKAF3DSGM3AB87EM9N0K41AJ',
            type: 'entropy',
            groups: {
              'entropy:01JKAF3DSGM3AB87EM9N0K41AJ/0': {
                id: 'entropy:01JKAF3DSGM3AB87EM9N0K41AJ/0',
                type: 'multichain-account',
                accounts: [selectedAccountAddress],
                metadata: {
                  name: 'Account 1',
                  entropy: {
                    groupIndex: 0,
                  },
                  hidden: false,
                  pinned: false,
                },
              },
            },
            metadata: {
              name: 'Wallet 1',
              entropy: {
                id: '01JKAF3DSGM3AB87EM9N0K41AJ',
              },
            },
          },
        },
        selectedAccountGroup: 'entropy:01JKAF3DSGM3AB87EM9N0K41AJ/0',
      },
      internalAccounts: {
        accounts: {
          [selectedAccountAddress]: {
            address: selectedAccountAddress,
            id: selectedAccountAddress,
            metadata: {
              name: 'Test Account',
              keyring: {
                type: 'HD Key Tree',
              },
            },
            options: {},
            methods: ETH_EOA_METHODS,
            type: EthAccountType.Eoa,
            scopes: [EthScope.Eoa],
          },
        },
        selectedAccount: selectedAccountAddress,
      },
      keyrings: [
        {
          type: KeyringType.hdKeyTree,
          accounts: ['0x1', '0x2'],
        },
        {
          type: KeyringType.ledger,
          accounts: [],
        },
      ],
      accountsAssets: {},
      useExternalServices: true,
    },
  };

  const store = configureMockStore([thunk])(mockStore);

  let openTabSpy: jest.SpyInstance;

  beforeAll(() => {
    jest.clearAllMocks();
    Object.defineProperty(global, 'platform', {
      value: {
        openTab: jest.fn(),
      },
    });
    openTabSpy = jest.spyOn(global.platform, 'openTab');
    setBackgroundConnection({
      getTokenSymbol: jest.fn(),
    } as never);
  });

  beforeEach(() => {
    openTabSpy.mockClear();

    // Mocking Date.now would not be sufficient, since it would render differently
    // depending on the machine's timezone. Mock the formatter instead.
    jest.spyOn(Intl, 'DateTimeFormat').mockImplementation(() => {
      return {
        format: () => 'Jun 9, 8:10 PM',
        resolvedOptions: jest.fn(),
        formatToParts: jest.fn(),
        formatRange: jest.fn(),
        formatRangeToParts: jest.fn(),
      };
    });
    // Clear previous mock implementations
    (useMultiPolling as jest.Mock).mockClear();

    // Mock implementation for useMultiPolling
    (useMultiPolling as jest.Mock).mockImplementation(({ input }) => {
      // Mock startPolling and stopPollingByPollingToken for each input
      const startPolling = jest.fn().mockResolvedValue('mockPollingToken');
      const stopPollingByPollingToken = jest.fn();

      input.forEach((inputItem: string) => {
        const key = JSON.stringify(inputItem);
        // Simulate returning a unique token for each input
        startPolling.mockResolvedValueOnce(`mockToken-${key}`);
      });

      return { startPolling, stopPollingByPollingToken };
    });
  });

  afterEach(() => {
    store.clearActions();
    jest.restoreAllMocks();
  });

  const native = {
    type: AssetType.native,
    chainId: '0x1',
    symbol: 'TEST',
    image: '',
    isOriginalNativeSymbol: true,
    balance: {
      value: '0',
      display: '0',
      fiat: '',
    },
    decimals: 18,
  } as const;

  const token = {
    type: AssetType.token,
    chainId: '0x1',
    address: '0xF0906D83c5a0bD6b74bC9b62D7D9F2014c6525C0',
    symbol: 'TEST',
    decimals: 18,
    image: '',
    balance: {
      value: '0',
      display: '0',
      fiat: '',
    },
  } as const;

  it('should not show a modal when token passed in props is not an ERC721', () => {
    renderWithProvider(<AssetPage asset={token} optionsButton={null} />, store);
    const actions = store.getActions();
    expect(actions).toHaveLength(0);
  });

  it('should show ConvertTokenToNFT modal when token passed in props is an ERC721', () => {
    renderWithProvider(
      <AssetPage asset={{ ...token, isERC721: true }} optionsButton={null} />,
      store,
    );
    const actions = store.getActions();
    expect(actions).toHaveLength(1);
    expect(actions[0].type).toBe('UI_MODAL_OPEN');
    expect(actions[0].payload).toStrictEqual({
      name: 'CONVERT_TOKEN_TO_NFT',
      tokenAddress: token.address,
    });
  });

  it('should enable the buy button on supported chains', () => {
    const { queryByTestId } = renderWithProvider(
      <AssetPage asset={token} optionsButton={null} />,
      store,
    );
    const buyButton = queryByTestId('token-overview-buy');
    expect(buyButton).toBeInTheDocument();
    expect(buyButton).toBeEnabled();
  });

  it('should disable the buy button on unsupported chains', () => {
    const { queryByTestId } = renderWithProvider(
      <AssetPage asset={token} optionsButton={null} />,
      configureMockStore([thunk])({
        ...mockStore,
        metamask: {
          ...mockStore.metamask,
          ...mockNetworkState({ chainId: CHAIN_IDS.SEPOLIA }),
        },
      }),
    );
    const buyButton = queryByTestId('token-overview-buy');
    expect(buyButton).toBeInTheDocument();
    expect(buyButton).toBeDisabled();
  });

  it('should open the buy crypto URL for a buyable chain ID', async () => {
    const mockedStoreWithBuyableChainId = {
      ...mockStore,
      metamask: {
        ...mockStore.metamask,
        ...mockNetworkState({ chainId: CHAIN_IDS.POLYGON }),
      },
    };
    const mockedStore = configureMockStore([thunk])(
      mockedStoreWithBuyableChainId,
    );

    const { queryByTestId } = renderWithProvider(
      <AssetPage asset={token} optionsButton={null} />,
      mockedStore,
    );
    const buyButton = queryByTestId('token-overview-buy');
    expect(buyButton).toBeInTheDocument();
    expect(buyButton).not.toBeDisabled();

    fireEvent.click(buyButton as HTMLElement);
    expect(openTabSpy).toHaveBeenCalledTimes(1);

    await waitFor(() =>
      expect(openTabSpy).toHaveBeenCalledWith({
        url: expect.stringContaining(`/buy?metamaskEntry=ext_buy_sell_button`),
      }),
    );
  });

  it('should show the Swap button if chain id is supported', async () => {
    const { queryByTestId } = renderWithProvider(
      <AssetPage asset={token} optionsButton={null} />,
      store,
    );
    const swapButton = queryByTestId('token-overview-swap');
    expect(swapButton).toBeInTheDocument();
    expect(swapButton).not.toBeDisabled();
  });

  it('should render Swap button on testnet chains', async () => {
    const { queryByTestId } = renderWithProvider(
      <AssetPage asset={token} optionsButton={null} />,
      configureMockStore([thunk])({
        ...mockStore,
        metamask: {
          ...mockStore.metamask,
          ...mockNetworkState({ chainId: CHAIN_IDS.SEPOLIA }),
        },
      }),
    );
    const swapButton = queryByTestId('token-overview-swap');
    expect(swapButton).toBeInTheDocument();
    expect(swapButton).not.toBeDisabled();
  });

  it('should render the network name', async () => {
    const mockedStore = configureMockStore([thunk])(mockStore);

    const { queryByTestId } = renderWithProvider(
      <AssetPage asset={token} optionsButton={null} />,
      mockedStore,
    );
    const networkNode = queryByTestId('asset-network');
    expect(networkNode).toBeInTheDocument();
    expect(networkNode?.textContent).toBe(MAINNET_DISPLAY_NAME);
  });

  it('should render a native asset', () => {
    const { container } = renderWithProvider(
      <AssetPage asset={native} optionsButton={null} />,
      store,
      '/0x1',
    );
    const dynamicImages = container.querySelectorAll('img[alt*="logo"]');
    dynamicImages.forEach((img) => {
      img.setAttribute('alt', 'static-logo');
    });
    expect(container).toMatchSnapshot();
  });

  it('should render an ERC20 asset without prices', async () => {
    const address = '0x309375769E79382beFDEc5bdab51063AeBDC4936';

    // Mock no price history
    nock('https://price.api.cx.metamask.io')
      .get(`/v1/chains/${CHAIN_IDS.MAINNET}/historical-prices/${address}`)
      .query(true)
      .reply(200, {});

    const { container, queryByTestId } = renderWithProvider(
      <AssetPage asset={{ ...token, address }} optionsButton={null} />,
      configureMockStore([thunk])({
        ...mockStore,
        metamask: {
          ...mockStore.metamask,
          marketData: {
            [CHAIN_IDS.MAINNET]: {
              [address]: {
                price: 123,
              },
            },
          },
        },
      }),
    );

    // Verify we show the loading state
    await waitFor(() => {
      const chart = queryByTestId('asset-chart-loading');
      expect(chart).toBeInTheDocument();
    });

    const dynamicImages = container.querySelectorAll('img[alt*="logo"]');
    dynamicImages.forEach((img) => {
      img.setAttribute('alt', 'static-logo');
    });
    const elementsWithAria = container.querySelectorAll('[aria-describedby]');
    elementsWithAria.forEach((el) =>
      el.setAttribute('aria-describedby', 'static-tooltip-id'),
    );

    expect(container).toMatchSnapshot();
  });

  it('should render an ERC20 token with prices', async () => {
    const address = '0xe4246B1Ac0Ba6839d9efA41a8A30AE3007185f55';
    const marketCap = 456;

    // Mock price history
    nock('https://price.api.cx.metamask.io')
      .get(`/v1/chains/${CHAIN_IDS.MAINNET}/historical-prices/${address}`)
      .query(true)
      .reply(200, { prices: [[1, 1]] });

    const { queryByTestId, container } = renderWithProvider(
      <AssetPage asset={{ ...token, address }} optionsButton={null} />,
      configureMockStore([thunk])({
        ...mockStore,
        metamask: {
          ...mockStore.metamask,
          marketData: {
            [CHAIN_IDS.MAINNET]: {
              [address]: {
                price: 123,
                marketCap,
                currency: 'ETH',
              },
            },
          },
        },
      }),
      '/0x1/0xe4246B1Ac0Ba6839d9efA41a8A30AE3007185f55',
    );

    // Verify chart is rendered
    await waitFor(() => {
      const chart = queryByTestId('asset-price-chart');
      expect(chart).toBeInTheDocument();
    });

    // Verify market data is rendered
    const marketCapElement = queryByTestId('asset-market-cap');
    expect(marketCapElement).toHaveTextContent('$56.09K');

    const dynamicImages = container.querySelectorAll('img[alt*="logo"]');
    dynamicImages.forEach((img) => {
      img.setAttribute('alt', 'static-logo');
    });
    const elementsWithAria = container.querySelectorAll('[aria-describedby]');
    elementsWithAria.forEach((el) =>
      el.setAttribute('aria-describedby', 'static-tooltip-id'),
    );
    expect(container).toMatchSnapshot();
  });
});
